package com.ylr.redis.framework.service;

import org.redisson.api.RLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Created by IntelliJ IDEA.
 *
 * @author wei
 * date: 2021-10-03 21:38:00
 * className: RedisUtils RedisTemplate模板操作工具类
 * version: 1.0
 * description:
 */
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisService {

    private static final Logger log = LoggerFactory.getLogger(RedisService.class);

    /**
     * Redis数据库操作模板对象
     */
    private final RedisTemplate redisTemplate;

    /**
     * Redis的key-value模糊删除策略，默认每次scan遍历的键值对个数
     */
    private final int defaultScanSize = 1000;

    @Autowired
    public RedisService(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 操作Redis数据库，删除Redis数据库中key及其对应的数据
     * @param key Redis数据键
     * @return 删除成功true，失败false
     */
    public Boolean delete(final String key) {
        log.debug("Redis基础操作 delete操作 key={}", key);
        return redisTemplate.delete(key);
    }

    /**
     * 操作Redis数据库，删除Redis数据库中keys包含的所有键及其对应的数据
     * 注意：
     *  1、如果删除的key存在，则将键值对删除，并且计数器加一
     *  2、如果删除的key不存在，则不进行任何操作，计数器不变
     * @param keys 所有待删除的key
     * @return 成功删除的key个数
     */
    public Long delete(final List<String> keys) {
        log.debug("Redis基础操作 delete操作 keys={}", keys);
        return redisTemplate.delete(keys);
    }

    /**
     * 操作Redis数据库，给key设置过期时间
     * @param key      Redis数据键
     * @param timeout  过期时间长度
     * @param timeUnit 过期时间类型
     * @return 设置成功返回true，失败false
     */
    public Boolean expire(final String key, final long timeout, TimeUnit timeUnit) {
        log.debug("Redis基础操作 expire操作 key={} timeout={} timeUnit={}", key, timeout, timeUnit);
        return redisTemplate.expire(key, timeout, timeUnit);
    }

    /**
     * 操作Redis数据库，给key设置过期时间
     * @param key  Redis数据键
     * @param date 过期时间
     * @return 设置成功返回true，失败false
     */
    public Boolean expireAt(final String key, final Date date) {
        log.debug("Redis基础操作 expire操作 key={} date={} ", key, date);
        return redisTemplate.expireAt(key, date);
    }

    /**
     * 操作Redis数据库，获取key指向数据的过期时间
     * 注意：
     *  1、返回结果为-1，则表示key指向数据没有过期时间
     * @param key Redis数据键
     * @return 剩余时间
     */
    public Long getExpire(final String key) {
        log.debug("Redis基础操作 getExpire操作 key={} ", key);
        return redisTemplate.getExpire(key);
    }

    /**
     * 操作Redis数据库，获取key指向数据的过期时间，并设置返回的时间类型格式
     * 注意：
     *  1、返回结果为-1，则表示key指向数据没有过期时间
     * @param key Redis数据键
     * @return 剩余时间
     */
    public Long getExpire(final String key, final TimeUnit timeUnit) {
        log.debug("Redis基础操作 getExpire操作 key={} timeUnit={}", key, timeUnit);
        return redisTemplate.getExpire(key, timeUnit);
    }

    /**
     * 操作Redis数据库，判断数据包含的所有键是否存在key
     * @param key Redis数据键
     * @return 存在返回true，不存在false
     */
    public Boolean hasKey(final String key) {
        log.debug("Redis基础操作 hasKey操作 key={} ", key);
        return redisTemplate.hasKey(key);
    }

    /**
     * 操作Redis数据库，将数据库键key设置成不会过期
     * 注意：
     *  1、如果key原来为永不过期，则此操作失败，返回false
     *  2、如果key在数据库中不存在，则此操作失败，返回false
     * @param key Redis数据键
     * @return 设置成功返回true，失败false
     */
    public Boolean persist(final String key) {
        log.debug("Redis基础操作 persist操作 key={}", key);
        return redisTemplate.persist(key);
    }

    /**
     * 操作Redis数据库，根据pattern规则获取数据库中的键
     * @param pattern 匹配规则
     * @return 符合规则的所有键
     */
    public Set<String> keys(final String pattern) {
        log.debug("Redis基础操作 keys操作");
        return redisTemplate.keys(pattern);
    }

    /**
     * 操作Redis数据库，‘随机’获取数据库中一个键
     * @return 数据库键
     */
    public String randomKey() {
        log.debug("Redis基础操作 randomKey操作");
        return (String) redisTemplate.randomKey();
    }

    /**
     * 操作Redis数据库，重命名数据库键oldKey为新名字newKey
     * @param oldKey 旧的键名
     * @param newKey 新的键名
     */
    public void rename(final String oldKey, final String newKey) {
        log.debug("Redis基础操作 rename操作 oldKey={} newKey={}", oldKey, newKey);
        redisTemplate.rename(oldKey, newKey);
    }

    /**
     * 操作Redis数据库，重命名数据库键oldKey为新名字newKey
     * 注意:
     *  1、当oldKey在数据库不存在时，不进行操作，返回null
     *  2、如果newKey在数据库存在，则操作失败，返回false
     * @param oldKey 旧的键名
     * @param newKey 新的键名
     */
    public Boolean renameIfAbsent(final String oldKey, final String newKey) {
        log.debug("Redis基础操作 renameIfAbsent操作 oldKey={} newKey={}", oldKey, newKey);
        return redisTemplate.renameIfAbsent(oldKey, newKey);
    }

    /**
     * Redis-lua脚本执行数据库键值对模糊删除
     * 单个模糊键删除策略
     * @param patternKey 模糊键
     * @return 成功删除元素个数
     */
    public Long fuzzyDelete(String patternKey) {
        return this.fuzzyDelete(patternKey, defaultScanSize);
    }

    /**
     * 解释同上
     * @param patternKey 模糊键
     * @param oneSize    scan一次遍历的键个数，即一次删除的键个数
     */
    public Long fuzzyDelete(String patternKey, long oneSize) {
        // 获取单个模糊键的删除lua脚本字符串
        String del = this.oneFuzzyKeyDeleteLua();
        // 指定 lua 脚本，并且指定返回值类型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(del, Long.class);
        // 参数一：redisScript，参数二：key列表，参数三：arg（可多个）
        return (Long) redisTemplate.execute(redisScript, Collections.singletonList(patternKey), oneSize);
    }

    /**
     * Redis-lua脚本执行数据库键值对模糊删除
     * 多个模糊键删除策略
     * @param patternKeys 存储多个模糊键的数组
     * @return 成功删除元素个数
     */
    public Long fuzzyDelete(String[] patternKeys) {
        return this.fuzzyDelete(patternKeys, defaultScanSize);
    }

    /**
     * 解释同上
     * @param patternKeys 存储多个模糊键的数组
     * @param oneSize     scan一次遍历的键个数，即一次删除的键个数
     */
    public Long fuzzyDelete(String[] patternKeys, long oneSize) {
        return this.fuzzyDelete(Arrays.asList(patternKeys), oneSize);
    }

    /**
     * 解释同上
     * @param patternKeys 存储多个模糊键的List集合
     */
    public Long fuzzyDelete(List<String> patternKeys) {
        return this.fuzzyDelete(patternKeys, defaultScanSize);
    }

    /**
     * 解释同上
     * @param patternKeys 存储多个模糊键的List集合
     * @param oneSize     scan一次遍历的键个数，即一次删除的键个数
     */
    public Long fuzzyDelete(List<String> patternKeys, long oneSize) {
        // 获取多个模糊键的删除lua脚本字符串
        String del = this.multiFuzzyKeyDeleteLua();
        // 指定 lua 脚本，并且指定返回值类型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(del, Long.class);
        // 参数一：redisScript，参数二：key列表，参数三：arg（可多个）
        return (Long) redisTemplate.execute(redisScript, patternKeys, oneSize);
    }

    /**
     * 尝试更新Redis键的过期时间
     * 如果键存在且键的剩余过期时间小于等于lessTtl值，则将键的最新过期时间设置为newExpire值
     * @param key       Redis键
     * @param lessTtl   更新的基准值
     * @param newExpire 最新的过期时间数值
     */
    public void tryUpdateKeyExpire(final RLock lock, final String key, final long lessTtl, final long newExpire) {
        boolean bool = false;
        try {
            // 尝试加锁
            bool = lock.tryLock();
            if (bool) {
                // 加锁成功，尝试更新Redis键的过期时间
                this.tryUpdateKeyExpire(key, lessTtl, newExpire);
            }
        } finally {
            // 判断当前锁对象是否还持有锁 并 持有该锁的线程与当前业务线程一致
            if (bool && lock.isLocked() && lock.isHeldByCurrentThread()) {
                // 解锁
                lock.unlock();
            }
        }
    }

    /**
     * 尝试更新Redis键的过期时间
     * 如果键存在且键的剩余过期时间小于等于lessTtl值，则将键的最新过期时间设置为newExpire值
     * @param key       Redis键
     * @param lessTtl   更新的基准值
     * @param newExpire 最新的过期时间数值
     * @return 操作结果
     */
    public Long tryUpdateKeyExpire(final String key, final long lessTtl, final long newExpire) {
        // 获取尝试更新redis-key过期时间脚本
        String luaString = this.tryUpdateKeyExpireLuaScript();
        // 指定 lua 脚本，并且指定返回值类型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaString, Long.class);
        // 参数一：redisScript，参数二：key列表，参数三：arg（可多个）
        return (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), lessTtl, newExpire);
    }

    /**
     * 比较并自增Redis-Hash存储的数字值
     * @param key     结构键
     * @param hashKey 数值键
     * @param num     阈值
     * @return 操作结果[null-操作失败 数字值-自增后的数字值]
     */
    public Long lessAndIncrement(String key, String hashKey, int num) {
        // 获取比较并自增Redis-Hash存储的数字值脚本
        String luaString = this.lessAndIncrementLuaScript();
        String result = (String) redisTemplate.execute((RedisConnection connection) ->
            connection.eval(
                // lua脚本
                luaString.getBytes(StandardCharsets.UTF_8),
                // 返回结果类型
                ReturnType.STATUS,
                // 键个数
                2,
                // 参数‘键’（1）
                key.getBytes(StandardCharsets.UTF_8),
                // 参数‘键’（2）
                hashKey.getBytes(StandardCharsets.UTF_8),
                // 参数‘值’（1）
                Integer.toString(num).getBytes(StandardCharsets.UTF_8)
            )
        );
        if (result != null) {
            return Long.parseLong(result);
        }
        return null;
    }

    /**
     * 是否开启Redis缓存
     * @return [true-开启 false-关闭]
     */
    public Boolean isOpen() {
        return true;
    }

    /**
     *
     * @param callback
     * @param <T>
     * @return
     */
    public <T> T execute(SessionCallback<T> callback) {
        return (T) redisTemplate.execute(callback);
    }

    /**
     * 单个模糊键的lua模糊删除脚本
     */
    private String oneFuzzyKeyDeleteLua() {
        // KEY[1]  为要删除的前缀  举例为 Test* 代表以Test开头的所有Key
        // ARGV[1] 单次遍历的数量 注意 不是返回的数量，举例 服务器总共Key有10万  设置为20000则需要循环5次才能全部遍历完
        return  "local function scan(key)\n" +
                "    local cursor = 0\n" +
                "    local keynum = 0\n" +
                "    local t = 0\n" +
                "    repeat\n" +
                "        local res = redis.call(\"scan\", cursor, \"match\", key,'COUNT',ARGV[1])\n" +
                "        if (res ~= nil and #res >= 0) then\n" +
                "            redis.replicate_commands()\n" +
                "            cursor = tonumber(res[1])\n" +
                "            local ks = res[2]\n" +
                "            keynum = #ks\n" +
                "            for i=1,keynum,1 do\n" +
                "                t = t + redis.call(\"unlink\", tostring(ks[i]))\n" +
                "            end\n" +
                "        end\n" +
                "    until (cursor <= 0)\n" +
                "    return t\n" +
                "end\n" +
                "local total = scan(KEYS[1])\n" +
                "return total";
    }

    /**
     * 多个模糊键的lua模糊删除脚本
     */
    private String multiFuzzyKeyDeleteLua() {
        return  "local function scan(key)\n" +
                "    local cursor = 0\n" +
                "    local keynum = 0\n" +
                "    local t = 0\n" +
                "    repeat\n" +
                "        local res = redis.call(\"scan\", cursor, \"match\", key, 'COUNT', ARGV[1])\n" +
                "        if (res ~= nil and #res >= 0) then\n" +
                "            redis.replicate_commands()\n" +
                "            cursor = tonumber(res[1])\n" +
                "            local list = res[2]\n" +
                "            keynum = #list\n" +
                "            for i=1,keynum,1 do\n" +
                "                t = t + redis.call(\"unlink\", tostring(list[i]))\n" +
                "            end\n" +
                "        end\n" +
                "    until (cursor <= 0)\n" +
                "    return t\n" +
                "end\n" +
                "local keys = #KEYS\n" +
                "local index = 1\n" +
                "local total = 0\n" +
                "while (index <= keys)\n" +
                "do\n" +
                "    total = total + scan(KEYS[index])\n" +
                "    index = index + 1\n" +
                "end\n" +
                "return total";
    }

    /**
     * 获取尝试更新redis-key过期时间脚本
     * 1、首先执行ttl操作，获取key的剩余时间
     * 2、判断ttl操作返回的结果
     *  2.1、如果key不存在，则ttl操作返回-2，直接返回结果-2
     *  2.2、如果key不设置过期时间，则ttl操作返回-1，直接返回结果-1
     *  2.2、ttl操作返回的key剩余时间与第一个请求参数比较
     *    2.2.1、小于等于第一个请求参数，程序进入if语句
     *      2.2.1.1、执行expire操作，更新key的过期时间，并返回操作结果（最新过期时间值为第二个请求参数）
     *    2.2.2、大于第一个请求参数，程序不进入if语句，且程序往下执行
     * 3、返回ttl操作结果（key剩余时间）
     * @return 脚本字符串
     */
    private String tryUpdateKeyExpireLuaScript() {
        /*
            KEYS[1]：操作的数据库键
            ARGV[1]：第一个请求参数
            ARGV[2]：第二个请求参数
         */
        String luaString =
                "local res = redis.call('TTL',KEYS[1]) " +
                        "if (res > 0 and res <= tonumber(ARGV[1])) then " +
                        "  res = redis.call('EXPIRE',KEYS[1],ARGV[2]) " +
                        "end " +
                        "return res";
        log.info("tryUpdateExpireLuaScript={}", luaString);
        return luaString;
    }

    /**
     * 生成比较并自增Redis-Hash存储的数字值脚本
     * 执行流程：
     * 1、获取Redis-Hash结构键与数值键关联的数字值
     *  1.1、如果返回结果为空 或者 返回结果小于参数设定的阈值，则目标值自增，并将自增后的结果返回（数字值）
     *  1.1、如果返回结果不为空 且 返回结果大于等于参数设定的阈值，则返回nil（空）
     * @return 脚本字符串
     */
    private String lessAndIncrementLuaScript() {
        /*
            KEYS[1]：Redis-Hash结构键
            KEYS[2]：数值键
            ARGV[1]：设定的阈值
         */
        String luaString =
                "local res = redis.call('HGET',KEYS[1],KEYS[2]) " +
                "if (res == false or tonumber(res) < tonumber(ARGV[1])) then" +
                "  return tostring(redis.call('HINCRBY',KEYS[1],KEYS[2],1)) " +
                "end " +
                "return nil";
        log.info("lessAndIncrementLuaScript={}", luaString);
        return luaString;
    }

}
